﻿using System;
using System.Drawing;
using System.Windows.Forms;
using System.Globalization;

namespace NullTodoList
{
    /// <summary>
    /// Календарь с тремя видами отображения: месяцы, дни, часы.
    /// </summary>
    public partial class DateTimeView : UserControl
    {
        private const Int32 COLOR_TAG_BORDER = 0xB3CEE1;
        private const Int32 COLOR_TAG_FILL = 0xE0EAF1;
        /// <summary>
        /// Отображение по часам.
        /// </summary>
        public const Int32 VIEW_MODE_HOURS = 0;
        /// <summary>
        /// Отображение по дням.
        /// </summary>
        public const Int32 VIEW_MODE_DAYS = 1;
        /// <summary>
        /// Отображение по месяцам.
        /// </summary>
        public const Int32 VIEW_MODE_MONTHS = 2;
        private const Int32 START_YEAR = 2000;
        // Текущий вид просмотра.
        private Int32 viewMode;
        // Флаг блокировки изменения даты.
        private Boolean locked;
        // Индекс первого дня месяца.
        private Int32 dayOffset;
        // Выбранная дата.
        private DateTime filterDate;
        //
        private TimeSpan time;
        // Счетчики записей.
        private Int32[] counters;

        private Random rand;

        #region ctor
        /// <summary>
        /// Конструктор по-умолчанию.
        /// </summary>
        public DateTimeView()
        {
            InitializeComponent();
            // Заполняем список годами с 2000 до 2099.
            for (var i = START_YEAR; i < START_YEAR + 100; i++)
                cbxYears.Items.Add(i.ToString());
            // Заполняем список месяцами.
            for (var i = 1; i < 13; i++)
                cbxMonths.Items.Add(CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(i));
            // 
            DateTimeToControls(DateTime.Now);
            ControlsToDateTime();
            // Переключаем вид на месяцы.
            lblHours_Click(lblMonthsView, EventArgs.Empty);

             rand = new Random();
        }
        #endregion

        #region Private methods
        /// <summary>
        /// Отображение выбранной даты в пользовательском интерфейсе.
        /// </summary>
        /// <param name="date">Дата.</param>
        private void DateTimeToControls(DateTime date)
        {
            if (locked)
                return;
            locked = true;
            try
            {
                try
                {
                    cbxYears.SelectedIndex = date.Year - START_YEAR;
                }
                catch (ArgumentOutOfRangeException)
                {
                    cbxYears.SelectedIndex = 0;
                }
                try
                {
                    cbxMonths.SelectedIndex = date.Month - 1;
                }
                catch (ArgumentOutOfRangeException)
                {
                    cbxMonths.SelectedIndex = 0;
                }
                try
                {
                    cbxDays.SelectedIndex = date.Day - 1;
                }
                catch (ArgumentOutOfRangeException)
                {
                    cbxDays.SelectedIndex = 0;
                }
            }
            finally
            {
                locked = false;
            }
        }

        /// <summary>
        /// Получение даты из пользовательского интерфейса.
        /// </summary>
        private void ControlsToDateTime()
        {
            if (locked)
                return;
            locked = true;
            try
            {
                // Запоминаем текущую дату.
                filterDate = new DateTime(cbxYears.SelectedIndex + START_YEAR,
                    cbxMonths.SelectedIndex + 1, cbxDays.SelectedIndex + 1, time.Hours, time.Minutes, time.Milliseconds);
                // Запоминаем с какого дня недели начинается месяц.
                dayOffset = DayOfWeekToInt(new DateTime(filterDate.Year, filterDate.Month, 1).DayOfWeek);
                // Оповещаем об изменении даты.
                OnDateChanged(EventArgs.Empty);
            }
            catch (ArgumentOutOfRangeException)
            {
            }
            finally
            {
                locked = false;
            }
            dateTimeGrid.Invalidate();
        }

        /// <summary>
        /// Перевод дня недели в индекс. 0 - понедельник, 6 - воскресенье.
        /// </summary>
        /// <param name="value">День недели.</param>
        /// <returns>Индекс.</returns>
        private Int32 DayOfWeekToInt(DayOfWeek value)
        {
            return value == DayOfWeek.Sunday ? 6 : (Int32)value - 1;
        }

        /// <summary>
        /// Перевод индекса дня в день недели. 0 - понедельник, 6 - воскресенье.
        /// </summary>
        /// <param name="value">Индекс дня.</param>
        /// <returns>День недели.</returns>
        private DayOfWeek IntToDayOfWeek(Int32 value)
        {
            return value == 6 ? DayOfWeek.Sunday : (DayOfWeek)(value + 1);
        }

        /// <summary>
        /// Перевод координат (x:y) ячейки в абсолютный индекс.
        /// Отсчет индекса начинается с 0, считается 
        /// слева направо, сверху вниз.
        /// </summary>
        /// <param name="cell">Координаты ячейки.</param>
        /// <returns>Абсолютный индекс ячейки.</returns>
        private Int32 CellToIndex(Point cell)
        {
            return cell.Y * dateTimeGrid.ColumnCount + cell.X;
        }

        /// <summary>
        /// Перевод абсолютного индекса ячейки в координаты (x:y).
        /// Отсчет индекса начинается с 0, считается 
        /// слева направо, сверху вниз.
        /// </summary>
        /// <param name="index">Абсолютный индекс ячейки.</param>
        /// <returns>Координаты ячейки.</returns>
        private Point IndexToCell(Int32 index)
        {
            return new Point(index % dateTimeGrid.ColumnCount, index / dateTimeGrid.ColumnCount);
        }

        /// <summary>
        /// Перевод координат (x:y) ячейки в число с учетом
        /// первого дня месяца и того, что первая строка
        /// отображает название дня недели.
        /// Отсчет дней с 1.
        /// </summary>
        /// <param name="cell">Координаты ячейки.</param>
        /// <returns>Число.</returns>
        private Int32 CellToDay(Point cell)
        {
            var day = CellToIndex(cell);
            day -= (6 + dayOffset);
            return day < 0 || day > cbxDays.Items.Count ? 0 : day;
        }

        /// <summary>
        /// Перевод числа в координаты (x:y) ячейки с учетом
        /// первого дня месяца и того, что первая строка
        /// отображает название дня недели.
        /// Отсчет дней с 1.
        /// </summary>
        /// <param name="day">Число.</param>
        /// <returns>Координаты ячейки.</returns>
        private Point DayToCell(Int32 day)
        {
            day += (6 + dayOffset);
            return IndexToCell(day);
        }

        /// <summary>
        /// Переключение режима отображения.
        /// Возможные режимы:
        /// VIEW_MODE_HOURS - часы
        /// VIEW_MODE_DAYS - дни
        /// VIEW_MODE_MONTHS - месяца
        /// </summary>
        /// <param name="mode"></param>
        private void SwitchViewMode(Int32 mode)
        {
            if (mode < VIEW_MODE_HOURS)
                viewMode = VIEW_MODE_HOURS;
            if (mode > VIEW_MODE_MONTHS)
                viewMode = VIEW_MODE_MONTHS;
            else
                viewMode = mode;

            Control activeControl = null;
            switch (viewMode)
            {
                case VIEW_MODE_HOURS:
                    dateTimeGrid.ColumnCount = 6;
                    dateTimeGrid.RowCount = 4;
                    dateTimeGrid.ActiveCell = IndexToCell(filterDate.Hour);
                    activeControl = lblHoursView;
                    break;
                case VIEW_MODE_DAYS:
                    dateTimeGrid.ColumnCount = 7;
                    dateTimeGrid.RowCount = 7;
                    dateTimeGrid.ActiveCell = DayToCell(filterDate.Day);
                    activeControl = lblDaysView;
                    break;
                case VIEW_MODE_MONTHS:
                    dateTimeGrid.ColumnCount = 4;
                    dateTimeGrid.RowCount = 3;
                    dateTimeGrid.ActiveCell = IndexToCell(filterDate.Month - 1);
                    activeControl = lblMonthsView;
                    break;
            }
            var parent = activeControl.Parent;
            foreach (Control control in parent.Controls)
                if (control == activeControl)
                    control.BackColor = MakeColor(COLOR_TAG_BORDER);
                else
                    control.BackColor = parent.BackColor;

            counters = new Int32[dateTimeGrid.RowCount * dateTimeGrid.ColumnCount];
            
            var x = 1f / dateTimeGrid.ColumnCount * 100;
            var y = 1f / dateTimeGrid.RowCount * 100;

            dateTimeGrid.ColumnStyles.Clear();
            dateTimeGrid.RowStyles.Clear();

            for (var i = 0; i < dateTimeGrid.ColumnCount; i++)
                dateTimeGrid.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, x));
            for (var i = 0; i < dateTimeGrid.RowCount; i++)
                dateTimeGrid.RowStyles.Add(new RowStyle(SizeType.Percent, y));

            lblMonths.Visible = viewMode < VIEW_MODE_MONTHS;
            cbxMonths.Visible = viewMode < VIEW_MODE_MONTHS;

            lblDays.Visible = viewMode < VIEW_MODE_DAYS;
            cbxDays.Visible = viewMode < VIEW_MODE_DAYS;

            // Оповещаем о смене режима просмотра.
            OnViewModeChanged(EventArgs.Empty);
        }

        /// <summary>
        /// Клик по метке переключения режима отображения.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void lblHours_Click(object sender, EventArgs e)
        {
            if (sender == lblHoursView)
                SwitchViewMode(VIEW_MODE_HOURS);
            else if (sender == lblDaysView)
                SwitchViewMode(VIEW_MODE_DAYS);
            else
                SwitchViewMode(VIEW_MODE_MONTHS);
        }

        /// <summary>
        /// Запрос на возможность выбора ячейки.
        /// Можно выбрать любую ячейку, кроме ячеек, отображающих названия дня недели.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void dateTimeGrid_ActiveCellChanging(object sender, ActiveCellChangingEventArgs e)
        {
            if (viewMode == VIEW_MODE_DAYS)
            {
                var day = CellToDay(e.Cell);
                e.Cancel = day == 0;
            }
        }

        /// <summary>
        /// Изменение даты/времени с помощью таблицы.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void dateTimeGrid_ActiveCellChanged(object sender, EventArgs e)
        {
            dateTimeGrid.Invalidate();
            try
            {
                switch (viewMode)
                {
                    case VIEW_MODE_HOURS:
                        time = new TimeSpan(CellToIndex(dateTimeGrid.ActiveCell), 0, 0);
                        ControlsToDateTime();
                        break;
                    case VIEW_MODE_DAYS:
                        cbxDays.SelectedIndex = CellToDay(dateTimeGrid.ActiveCell) - 1;
                        break;
                    case VIEW_MODE_MONTHS:
                        cbxMonths.SelectedIndex = CellToIndex(dateTimeGrid.ActiveCell);
                        break;
                }
            }
            catch (ArgumentOutOfRangeException)
            {
            }
        }

        /// <summary>
        /// Отрисовка таблицы.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void dateTimeGrid_CellPaint(object sender, TableLayoutCellPaintEventArgs e)
        {
            // Активная ячейка.
            if (dateTimeGrid.ActiveCell == new Point(e.Column, e.Row))
            {
                var pen = new Pen(MakeColor(COLOR_TAG_BORDER));
                var brush = new SolidBrush(MakeColor(COLOR_TAG_FILL));
                e.Graphics.FillRectangle(brush, e.CellBounds);
                e.Graphics.DrawRectangle(pen, e.CellBounds);
            }

            var str = String.Empty;
            var currentCell = new Point(e.Column, e.Row);
            var font = dateTimeGrid.Font;
            var textFlags = TextFormatFlags.Right | TextFormatFlags.Top;
            var drawRecordCount = true;
            switch (viewMode)
            {
                case VIEW_MODE_HOURS:
                    str = String.Format("{0:D2}:00-{0:D2}:59", CellToIndex(currentCell));
                    break;
                case VIEW_MODE_DAYS:
                    if (e.Row == 0)
                    {
                        font = new Font(font, FontStyle.Italic | FontStyle.Bold);
                        textFlags = TextFormatFlags.VerticalCenter | TextFormatFlags.HorizontalCenter;
                        str = CultureInfo.CurrentCulture.DateTimeFormat.GetDayName(IntToDayOfWeek(e.Column));
                        drawRecordCount = false;
                    }
                    else
                    {
                        var index = CellToDay(currentCell);
                        if (index > 0)
                            str = index.ToString();
                        else
                            drawRecordCount = false;
                    }
                    break;
                case VIEW_MODE_MONTHS:
                    str = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(CellToIndex(currentCell) + 1);
                    break;
            }
            TextRenderer.DrawText(e.Graphics, str, font, e.CellBounds, Color.Black, textFlags);
            if (drawRecordCount)
            {
                var count = counters[CellToIndex(currentCell)];
                font = new Font(font.FontFamily, 7);
                str = count == 0 ? "Записей нет" : String.Format("Записей: {0}", count);
                TextRenderer.DrawText(e.Graphics, str, font, e.CellBounds, Color.Black, TextFormatFlags.Left | TextFormatFlags.Bottom);
            }
        }

        /// <summary>
        /// Изменение года, либо месяца с помощью ComboBox.
        /// Корректируем количество дней для выбранного месяца и
        /// запоминаем новое значение даты.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void cbxYears_SelectedIndexChanged(object sender, EventArgs e)
        {
            cbxDays.BeginUpdate();
            try
            {
                // Сохраняем индекс в списке дней.
                var save = cbxDays.SelectedIndex;
                // Получаем количество дней в выбранном месяце.
                var daysInMonth = DateTime.DaysInMonth(cbxYears.SelectedIndex + START_YEAR, cbxMonths.SelectedIndex + 1);
                // Разница в днях между новым и старом месяцами.
                var delta = daysInMonth - cbxDays.Items.Count;
                if (delta > 0)
                {
                    // В новом месяце больше дней, чем в старом. Добавляем новые дни.
                    for (var i = cbxDays.Items.Count; i < daysInMonth; i++)
                        cbxDays.Items.Add(i + 1);
                }
                else if (delta < 0)
                {
                    // В новом месяце меньше дней, чем в старом. Удаляем лишние дни.
                    while (delta != 0)
                    {
                        cbxDays.Items.RemoveAt(cbxDays.Items.Count - 1);
                        delta++;
                    }
                }
                // Корректируем при надобности день для нового месяца,
                // чтобы автоматом вызовет сохранение новой даты, либо
                // вручную сохраняем дату.
                if (save >= cbxDays.Items.Count)
                    cbxDays.SelectedIndex = cbxDays.Items.Count - 1;
                else
                    ControlsToDateTime();
                // Если сейчас отображаются дни, то корректируем
                // активную ячейку.
                if (viewMode == VIEW_MODE_DAYS)
                    dateTimeGrid.ActiveCell = DayToCell(filterDate.Day);
            }
            catch (ArgumentOutOfRangeException)
            {
            }
            finally
            {
                cbxDays.EndUpdate();
            }
        }

        /// <summary>
        /// Изменение дня с помощью ComboBox.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void cbxDays_SelectedIndexChanged(object sender, EventArgs e)
        {
            ControlsToDateTime();
        }
        #endregion

        #region Protected methods
        /// <summary>
        /// Вызов события-уведомления о начале рисования ячейки.
        /// </summary>
        /// <param name="e">Параметр рисования.</param>
        protected virtual void OnBeforeCellPaint(TableLayoutCellPaintEventArgs e)
        {
            if (BeforeCellPaint != null)
                BeforeCellPaint(this, e);
        }

        /// <summary>
        /// Вызов события-уведомления об окончании рисования ячейки.
        /// </summary>
        /// <param name="e">Параметр рисования.</param>
        protected virtual void OnAfterCellPaint(TableLayoutCellPaintEventArgs e)
        {
            if (AfterCellPaint != null)
                AfterCellPaint(this, e);
        }

        /// <summary>
        /// Вызов события-уведомления об изменении текущей даты.
        /// </summary>
        /// <param name="e">Параметр рисования.</param>
        protected virtual void OnDateChanged(EventArgs e)
        {
            if (DateChanged != null)
                DateChanged(this, e);
        }

        /// <summary>
        /// Вызов события-уведомления о режиме просмотра.
        /// </summary>
        /// <param name="e">Параметр рисования.</param>
        protected virtual void OnViewModeChanged(EventArgs e)
        {
            if (ViewModeChanged != null)
                ViewModeChanged(this, e);
        }
        #endregion

        #region Public methods
        /// <summary>
        /// Установка значения счетчика для указанного ИД.
        /// </summary>
        /// <param name="id">ИД счетчика.</param>
        /// <param name="value">Значение.</param>
        public void SetCounter(Int32 id, Int32 value)
        {
            switch (viewMode)
            {
                case VIEW_MODE_HOURS:
                    break;
                case VIEW_MODE_DAYS:
                    id += (6 + dayOffset); 
                    break;
                case VIEW_MODE_MONTHS:
                    id--;
                    break;
            }
            if (id >= 0 && id < counters.Length)
                counters[id] = value;
        }

        #endregion

        #region Properties
        /// <summary>
        /// Текущий режим отображения.
        /// </summary>
        public Int32 ViewMode
        {
            get { return viewMode; }
            set { SwitchViewMode(value); }
        }

        /// <summary>
        /// Выбрання дата/время.
        /// </summary>
        public DateTime FilterDateTime
        {
            get { return filterDate; }
        }
        #endregion

        #region Events
        /// <summary>
        /// Событие начала отрисовки клетки.
        /// </summary>
        public event EventHandler<TableLayoutCellPaintEventArgs> BeforeCellPaint;
        /// <summary>
        /// Событие окончания отрисовки клетки.
        /// </summary>
        public event EventHandler<TableLayoutCellPaintEventArgs> AfterCellPaint;
        /// <summary>
        /// Событие изменения даты.
        /// </summary>
        public event EventHandler DateChanged;
        /// <summary>
        /// Событие изменения режима просмотра.
        /// </summary>
        public event EventHandler ViewModeChanged;
        #endregion

        /// <summary>
        /// Создание полностью непрозрачного цвета (alpha=255) из Int32.
        /// </summary>
        /// <param name="value"></param>
        /// <returns></returns>
        public static Color MakeColor(Int32 value)
        {
            return Color.FromArgb(0xFF, value >> 16 & 0xFF, value >> 8 & 0xFF, value & 0xFF);
        }
    }
}
